package com.liuxing.log;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.util.concurrent.CompletableFuture;

/**
 * desc: 通过切面和注解进行日志记录
 *
 * @author 刘磊
 * @since  2020/4/23
 */
@Aspect
@Component
public class LogAspect {

    //脚本引擎
    private static final ScriptEngineManager engineManager = new ScriptEngineManager();
    private static final String SCRIPT_ENGINE_TYPE = "JavaScript";
    private static final String SCRIPT_TEMP_VAL_NAME = "_obj";

    @Pointcut("@annotation(com.liuxing.log.Log)")
    public void pointCut() {
    }

    @Around(value = "pointCut()&&@annotation(log)")
    public Object around(ProceedingJoinPoint point, Log log) {
        Object rst;
        try {
            rst = point.proceed();
            recordLog(point, rst, log);
            return rst;
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    }

    /**
     * 记录日志， 日志需要记录哪些信息可以自行定义
     *
     * @param point 切面
     * @param rst   方法执行结果
     */
    private void recordLog(ProceedingJoinPoint point, Object rst, Log log) {
        // 通过shiro获取用户信息，如果不使用shiro也可以通过threadlocal的方式在过滤器中将登录信息进行记录，此处再获取
        Subject subject = SecurityUtils.getSubject();
        // 此处可以根据项目定义日志bean
        JSONObject liteLog = new JSONObject();
        CompletableFuture.runAsync(() -> {
            if (log != null) {
                // 日志处理
                String condition = log.condition();
                Object[] args = point.getArgs();
                // 判断条件是否需要记录日志
                if (!dealCondition(condition, args, rst)) {
                    return;
                }
                String host = subject.getSession().getHost();
                // 远端IP
                liteLog.put("ip", host);
                // 登录用户
                JSONObject userInfo = (JSONObject) subject.getPrincipal();
                liteLog.put("admin", userInfo.get("name"));
                // 日志类型
                liteLog.put("type", log.type());
                // 日志动作
                liteLog.put("action", log.action());
                // 日志关联业务表ID
                if (log.key().length() > 0) {
                    String key = log.key();
                    // 处理表达式
                    Integer keyId = dealKey(key, args);
                    if (keyId != null) {
                        liteLog.put("keyId", keyId);
                    }
                }
                // 日志详细内容
                liteLog.put("comment", dealComment(log.comment(), args, userInfo));
                // 是否记录方法返回结果
                if (log.result()) {
                    String rstStr = JSON.toJSONString(rst);
                    // 如果表内有长度限制，需要做截断处理
                    liteLog.put("result", rstStr.length() > 120 ? rstStr.substring(0, 120) : rstStr);
                }
                // 保存日志
                System.out.println("save log: " + liteLog.toJSONString());
            }
        });
    }

    /**
     * 处理备注信息, 提供自带占位符，%user: 登录用户，%role: 角色
     *
     * @param comment 备注
     * @return 处理后的备注
     */
    private String dealComment(String comment, Object[] args, JSONObject userInfo) {
        String rst = comment;
        if (comment.contains("%user") && userInfo != null) {
            rst = rst.replace("%user", userInfo.get("name").toString());
        }
        if (comment.contains("%role") && userInfo != null) {
            rst = rst.replace("%role", userInfo.get("role").toString());
        }
        // 处理方法参数占位符
        for (int i = 1; i <= args.length; i++) {
            String s = "%" + i;
            if (comment.contains(s)) {
                rst = rst.replace(s, args[i - 1].toString());
            }
        }
        return rst;
    }

    /**
     * 处理是否需要记录日志的条件
     *
     * @param condition 条件表达式
     * @param args      参数列表
     * @param rst       方法返回结果
     */
    private boolean dealCondition(String condition, Object[] args, Object rst) {
        ScriptEngine engine = engineManager.getEngineByName(SCRIPT_ENGINE_TYPE);
        // 检查是否包含参数
        for (int i = 1; i < args.length; i++) {
            String s = "%" + i;
            if (condition.contains(s)) {
                boolean isObject = condition.contains(s + ".");
                String rps = SCRIPT_TEMP_VAL_NAME + i;
                condition = condition.replace(s, rps);
                engine.put(rps, isObject ? JSON.parseObject(JSON.toJSONString(args[i - 1])) : args[i - 1]);
            }
        }
        // 检查是否包含结果
        if (condition.contains("%r")) {
            String rps = SCRIPT_TEMP_VAL_NAME + "r";
            condition = condition.replace("%r", rps);
            engine.put(rps, condition.contains("%r.") ? JSON.parseObject(JSON.toJSONString(rst)) : rst);
        }
        try {
            return (boolean) engine.eval(condition);
        } catch (ScriptException e) {
            return false;
        }
    }

    /**
     * 通过动态脚本对key进行解析
     *
     * @param key  key串
     * @param args 参数
     */
    private Integer dealKey(String key, Object[] args) {
        for (int i = 1; ; i++) {
            String s = "%" + i;
            if (key.contains(s)) {
                try {
                    ScriptEngine engine = engineManager.getEngineByName(SCRIPT_ENGINE_TYPE);
                    boolean isObject = key.contains(s + ".");
                    key = key.replace(s, SCRIPT_TEMP_VAL_NAME + i);
                    engine.put(SCRIPT_TEMP_VAL_NAME + i, isObject ? JSON.parseObject(JSON.toJSONString(args[i - 1])) : args[i - 1]);
                    Object obj = engine.eval(key);
                    if (obj instanceof Integer) {
                        return (Integer) obj;
                    } else {
                        return null;
                    }
                } catch (ScriptException e) {
                    return null;
                }
            }
        }
    }
}
